package task;

import com.graphhopper.util.DistanceCalc;
import com.graphhopper.util.DistancePlaneProjection;
import edu.buaa.act.helper.Utils;
import edu.buaa.act.model.GPSPoint;
import edu.buaa.act.helper.CsvGPSFileReader;
import edu.buaa.act.anonymous.coarseGrained.RegionMetrics;
import edu.buaa.act.helper.RawDataCleaning;
import org.slf4j.Logger;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

import static edu.buaa.act.helper.CsvGPSFileReader.*;

/**
 * Created by T7810 on 2017/5/27.
 *
 * Usage:
 *   java -jar temporal-model.jar task.CalcUnicityUCar path-to-trajectory-file space-range-in-meter time-range-in-second select-points-count repeat-calc-times
 *
 * Example:
     *   java -cp temporal-model.jar task.CalcUnicityUCar D:\trajectory-file.txt 200 3600 2 10
 *
 * Explain:
 * This would read data file D:\trajectory-file.txt ,
 * set space range radius to 200 meters,
 * set temporal range to 3600 seconds,
 * will select 2 points,
 * repeat this calculation 10 times.
 *
 */
public class CalcOldUnicity
{
    private static Logger log = Utils.getDebugLogger();
    private static Random random = new Random(System.currentTimeMillis());
    private static DistanceCalc distanceCalc = new DistancePlaneProjection();

    public static void main(String[] args)
    {

        try {
            String filePath = args[0];
            int r = Integer.parseInt(args[1]); // radius of geography anonymous range (in meter)
            int t = Integer.parseInt(args[2])*1000; // temporal anonymous range (in second)
            int k = Integer.parseInt(args[3]); // sample k points.
            int q = Integer.parseInt(args[4]); // repeat q times.
            int parallelCount = Integer.parseInt(args[5]);

            log.debug("file: {}", filePath);
            log.debug("point count {}, time interval {}, r {}, repeat {} times, using {} CPUs", k, t, r, q, parallelCount);

            List<List<GPSPoint>> rawData = readData(new File(filePath));
            List<List<Long>> sampleData = convert(rawData, t, r, parallelCount);
            List<Set<Long>> dataToHit = listToSet(sampleData);
//            System.exit(0);
            float sum = 0;
            for (int i = 0; i < q; i++) {
                float u = unicity(sampleData, dataToHit, k, parallelCount);
                sum += u;
                log.debug(String.format("Unicity %f", u));
            }
            log.info("(point count, time interval, r, AVG Unicity of {} time): {} {} {} {}", q, k, t, r, sum / q);
        }catch (Throwable e){
            e.printStackTrace();
        }
    }

    private static List<Set<Long>> listToSet(List<List<Long>> rawData) {
        List<Set<Long>> result = new ArrayList<>();
        for(List<Long> car : rawData){
            Set<Long> set = new HashSet<>();
            set.addAll(car);
            result.add(set);
        }
        return result;
    }

    private static List<List<Long>> convert(List<List<GPSPoint>> data, int t, int r, int parallelCount) {
        log.trace("convert start");
        double minLat=Double.MAX_VALUE;
        double maxLat=Double.MIN_VALUE;
        double minLon=Double.MAX_VALUE;
        double maxLon=Double.MIN_VALUE;
        long minTime=Long.MAX_VALUE;
        long maxTime=Long.MIN_VALUE;

        for(List<GPSPoint> car : data){
            for(GPSPoint p : car){
                if(minLat > p.getLatitude()) minLat = p.getLatitude();
                if(maxLat < p.getLatitude()) maxLat = p.getLatitude();
                if(minLon > p.getLongitude()) minLon = p.getLongitude();
                if(maxLon < p.getLongitude()) maxLon = p.getLongitude();
                if(minTime > p.getTimestamp()) minTime = p.getTimestamp();
                if(maxTime < p.getTimestamp()) maxTime = p.getTimestamp();
            }
        }

        log.trace("car({}) lat({} {}) lon({} {}) time({} {})", data.size(), minLat, maxLat, minLon, maxLon, minTime, maxTime);

        RegionMetrics regionMetrics = new RegionMetrics(minLon, maxLon, minLat, maxLat, minTime, maxTime, r, t);

        BlockingQueue<List<Long>> result = new LinkedBlockingQueue<>();
        Semaphore semaphore = new Semaphore(parallelCount);
        for(List<GPSPoint> car : data){
            new ConvertOneCar(car, regionMetrics, result, semaphore).start();
        }

        long lastTime = System.currentTimeMillis();

        while (semaphore.hasQueuedThreads() || result.size()<data.size()) {
            if (System.currentTimeMillis() - lastTime > 100_000) {
                lastTime = System.currentTimeMillis();
                log.trace("remain({})", semaphore.getQueueLength());
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return new ArrayList<>(result);
    }

    private static class ConvertOneCar extends Thread
    {
        private final List<GPSPoint> selectedCar;
        private final Semaphore semaphore;
        private final BlockingQueue<List<Long>> resultQ;
        private final RegionMetrics regions;

        ConvertOneCar(List<GPSPoint> selectedCarData, RegionMetrics regionMetrics, BlockingQueue<List<Long>> resultQueue, Semaphore semaphore){
            this.selectedCar = selectedCarData;
            this.regions = regionMetrics;
            this.resultQ = resultQueue;
            this.semaphore = semaphore;
        }
        @Override
        public void run()
        {
            try {
                semaphore.acquire();
                List<Long> set = new ArrayList<>();
                for(GPSPoint p : this.selectedCar)
                {
                    set.add(this.regions.next(p));
                }
                resultQ.add(set);
            } catch (InterruptedException | IOException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    }

    private static List<List<GPSPoint>> readData(File file) throws IOException
    {
        List<GPSPoint> allData = CsvGPSFileReader.readLargeFile(file, " ", CAR_ID, LATITUDE, LONGITUDE, TIMESTAMP);
        return RawDataCleaning.separateTrajByCarID(allData);
    }

    private static List<Long> sample(int pointCount, List<Long> carData)
    {
        Set<Integer> sampleIndexSet = new HashSet<>();
        while(sampleIndexSet.size()<pointCount)
        {
            int index = random.nextInt(carData.size());
            sampleIndexSet.add(index);
        }
        List<Long> samplePoints = new ArrayList<>();
        for(Integer i : sampleIndexSet)
        {
            Long p = carData.get(i);
            samplePoints.add(p);
        }
        return samplePoints;
    }

    private static boolean isUnique(int selectedCar, List<Set<Long>> allRawData, List<Long> samplePoints)
    {
        for(int i=0; i<allRawData.size(); i++)
        {
            if(i!=selectedCar)
            {
                Set<Long> carData = allRawData.get(i);
                if(overlap(carData, samplePoints))
                {
                    return false;
                }
            }
        }
        return true;
    }

    private static class IsUnique extends Thread
    {

        private final int selectedCar;
        private final List<Set<Long>> allRawData;
        private final List<Long> samplePoints;
        private final AtomicInteger uniqueCount;
        private final Semaphore semaphore;
        private final AtomicInteger complete;

        IsUnique(int selectedCar, List<Set<Long>> allRawData, List<Long> samplePoints, AtomicInteger uniqueCount, Semaphore semaphore, AtomicInteger completeCount){
            this.selectedCar = selectedCar;
            this.allRawData = allRawData;
            this.samplePoints = samplePoints;
            this.uniqueCount = uniqueCount;
            this.semaphore = semaphore;
            this.complete = completeCount;
        }
        @Override
        public void run()
        {
            try {
                semaphore.acquire();
                for(int i=0; i<allRawData.size(); i++)
                {
                    if(i!=selectedCar)
                    {
                        Set<Long> carData = allRawData.get(i);
                        if(overlap(carData, samplePoints))
                        {
                            return;
                        }
                    }
                }
                uniqueCount.getAndIncrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                complete.getAndIncrement();
                semaphore.release();
            }

        }
    }

    private static float unicity(List<List<Long>> allListData, List<Set<Long>> allSetData, int pointCount, int parallelCount)
    {
        Semaphore semaphore = new Semaphore(parallelCount);
        AtomicInteger uniqueCount = new AtomicInteger(0);
        AtomicInteger completeCount = new AtomicInteger(0);
        for(int i=0; i<allListData.size(); i++)
        {
            List<Long> carData = allListData.get(i);
            List<Long> samples = sample(pointCount, carData);
            new IsUnique(i, allSetData, samples, uniqueCount, semaphore, completeCount).start();
        }

        long lastTime = System.currentTimeMillis();

        while (semaphore.hasQueuedThreads() || completeCount.get()<allSetData.size()) {
            if (System.currentTimeMillis() - lastTime > 100_000) {
                lastTime = System.currentTimeMillis();
                log.debug("uniqueCount({}) remain({})", uniqueCount.get(), semaphore.getQueueLength());
            }
        }

        return uniqueCount.get() * 1f / allSetData.size();
    }

    private static boolean overlap(Set<Long> carData, List<Long> sampleIndexSet)
    {
        for(Long toMatch : sampleIndexSet)
        {
            if(!findOnePoint(carData, toMatch)){
                return false;
            }
        }
        return true;
    }

    private static boolean findOnePoint(Set<Long> carData, Long toMatch)
    {
        return carData.contains(toMatch);
    }


//    private static class SP //simple gps point, timestamp to time slot, latitude longitude to region id
//    {
//        private int timeSlot;
//        private int regionId;
//
//        public SP(int timeSlot, int regionId) {
//            this.timeSlot = timeSlot;
//            this.regionId = regionId;
//        }
//
//        @Override
//        public boolean equals(Object o) {
//            if (this == o) return true;
//            if (o == null || getClass() != o.getClass()) return false;
//
//            SP sp = (SP) o;
//
//            if (timeSlot != sp.timeSlot) return false;
//            return regionId == sp.regionId;
//        }
//
//        @Override
//        public int hashCode() {
//            int result = timeSlot;
//            result = 31 * result + regionId;
//            return result;
//        }
//    }
}
